/*
 * @file unitTest.c
 *
 * @brief These functions provide a framework for unit testing in C.
 * A test suite can be viewed as unit testing a C source file.
 * A test case can be viewed as testing an individual function.
 * 
 * For an example of how to use the API of the framework, see
 * the comments in unitTest.h
 * 
 * This program is free software. You can redistribute it and/or modify it
 * under the terms of the GNU General Public License, version 3 (GPLv3).
 *
 */
#include <stdio.h>      // to provide vprintf/vfprintf/snprintf
#include <stdarg.h>     // to allow for variable argument functions
#include <stdbool.h>    // allow the use of boolean data type
#include "unitTest.h"   // common declarations to the unit test project

#ifdef FLOATING_POINT_NEEDED
#include <math.h>       // to provide fabs
#endif

/**
 * @attention - if using MSC, #define snprintf _snprintf
 */

/**
 * maximum length of an error message
 */
#define MAX_MSG_LEN ((int) 100)

/**
 *  Unit test results are logged to a file.
 */
static FILE *logFile = 0;

/**
 * true   if a test suite is currently executing
 * false  if a test suite is currently NOT executing
 */
static bool isTestSuiteActive = false;

/**
 * true   if a test case is currently executing
 * false  if a test case is currently NOT executing
 */
static bool isTestCaseActive = false;

/**
 * true   if the current test case passed all assertions
 * false  if at least one test case assertion failed
 */
static bool currentTestCasePass = true;

/**
 * if any asserts fail during a run, this is set to true
 */
static bool failed_assert = false;

/**
 * Static function declarations
 */
static void assertFailed(const char * , int, const char *);
static void logMessage(char const *formatString, ...);

/**
 *  Call this function to indicate the start of a new test suite.
 *  A test suite can be viewed as unit testing a C source file.
 *
 *  @attention - Remember to call endTestSuite when a test suite is complete.
 *     
 *  @param testSuitName - string containing the name of the test suite.
 *  In most cases, this would contain the name of the file being unit
 *  tested.
 *
 */
void startTestSuite (const char *testSuiteName)
{
  static int testSuiteNumber = 0;
 
  if (!isTestSuiteActive)
  {
    /*
     * Create a unique number for each test suite.
     * This is to make it easier to refer to the output
     * when analyzing the results.
     */ 
    logMessage("\n\nTest Suite Number: %d", ++testSuiteNumber);
    logMessage("\nTest Suite Name: %s", testSuiteName);
   
    isTestSuiteActive = true;
  }
  else
  {
    // A test suite is already active: Bad use of API!
    logMessage("\n\nERROR: A test suite is already active.");
    logMessage("\nCannot execute \"%s\"", testSuiteName);
    logMessage("\nOnly one test suite can be executed at a time.\n");
  }
}

/**
 *  Call this to indicate a test suite has completed. The framework
 *  checks to see if a test suite is active and logs the appropriate
 *  message.
 */
void endTestSuite (void)
{
  if (isTestSuiteActive)
  {
    logMessage("\n\nTest Suite Complete\n");
    isTestSuiteActive = false;
  }
  else
  {
    // A test suite is not active: Bad use of API!
    logMessage("\n\nERROR: A test suite is not active.");
    logMessage("\nCall 'startTestSuite' first.\n");
  }
}

/**
 *  Call this to indicate the start of a new test case. If any of
 *  the asserts fail for a test case, then the entire test case fails.
 *       
 *  @attention - Remember to call endTestCase when you are done with a test case.
 * 
 *  @param testCaseName - string containing the name of the test case.
 *  In most cases this would contain the name of the function being tested.
 *
 */
void startTestCase (const char *testCaseName)
{ 
  if (!isTestCaseActive)
  {
    logMessage("\n\nTest Case: %s", testCaseName);
   
    // reset the flag indicating the test case result
    currentTestCasePass = true;
   
    // set the flag indicating that a test case is active
    isTestCaseActive = true;
  }
  else
  {
    // A test case is already active: Bad use of API!
    logMessage("\n\nERROR: A test case is already active.");
    logMessage("\nCannot execute \"%s\"", testCaseName);
    logMessage("\nOnly one test case can be executed at a time.\n");
  }
}

/**
 *  Call this to indicate a test case has completed. The framework
 *  checks to see if the test case passed all assertions and logs
 *  the appropriate message.
 */
void endTestCase (void)
{
  if (isTestCaseActive)
  {
    if (currentTestCasePass)
    {
      logMessage("\nTest Case Passed"); 
    }
    else
    {
      logMessage("\nTest Case Failed");   
    }       

    isTestCaseActive = false;
  }
  else
  {
    // A test case is not active: Bad use of API!
    logMessage("\n\nERROR: A test case is not active.");
    logMessage("\nCall the 'startTestCase' function first.\n");
  }
}

/**
 * @return true if all unit test cases have passed in a run, false otherwise.
 */
bool unitTestAllSuccess (void)
{
    return (failed_assert) ? false : true;
}

/**
 *  If logging is turned on, log a message to stdout and to the log file.
 *
 *  @param formatString  printf style format string
 *  @param ...           variable number of arguments to match the format
 *
 */
/*lint -e{438} last value assigned to 'ap' not used */
/*lint -e{778} constant expression evaluates to zero in va_start */
static void logMessage (char const *formatString, ...)
{
  va_list ap;

  if(logFile)
  { 
    va_start (ap, formatString);
   
    /*
     * print to standard output and write to file
     */
    (void) vprintf(formatString, ap);
    (void) vfprintf(logFile, formatString, ap );
   
    va_end (ap); 
  }
}

/**
 *  Open the unit test log file. This function should be
 *  called prior to any test suites being called.
 *
 *  @param  fileName name of the file to open
 *
 */
void unitTestLogOn (char const *fileName)
{
  if(fileName)
  {
    logFile = fopen( fileName, "w" );

    // track whether there are any failed asserts during the run
    failed_assert = false;
  }
}

/**
 *  Close the unit test log file. This should be called only after all
 *  the test suites have been executed.
 */
void unitTestLogOff (void)
{
  if(logFile)
  {
    (void) fclose(logFile);
    logFile = 0;
  }
}

/**
 *  This function is called when an assertion fails. An
 *  error message is logged and test case failure is noted.
 * 
 *  @param file   the source file name
 *  @param lineNo the source code line number
 *  @param msg    the message to be displayed
 *
 */
static void assertFailed (const char *file, int lineNo, const char *msg)
{
  logMessage("\n    Assert Failed in File: %s, Line %d: %s",
             file, lineNo, msg);
     
  // the current test case has failed
  currentTestCasePass = false;

  // if any unit test case fails, this is set to true
  failed_assert = true;
}

/**
 *  Asserts if the bool values are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqBool (bool expected, bool actual,
                  const char *file , int lineNo)
{      
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the bool values are equal.
 *   
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqBool (bool expected, bool actual,
                      const char *file , int lineNo)
{      
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the chars are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqChar (char expected, char actual,
                  const char *file , int lineNo)
{       
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the chars are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqChar (char expected, char actual,
                     const char *file , int lineNo)
{      
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the unsigned chars are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqUnsignedChar (unsigned char expected, unsigned char actual,
                          const char *file , int lineNo)
{       
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %u, got: %u", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the unsigned chars are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqUnsignedChar (unsigned char expected, unsigned char actual,
                             const char *file , int lineNo)
{      
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %u, got: %u", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the shorts are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqShort (short expected, short actual,
                   const char *file , int lineNo)
{       
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the shorts are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *     
 */      
void assertNotEqShort (short expected, short actual,
                      const char *file , int lineNo)
{      
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the unsigned shorts are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqUnsignedShort (unsigned short expected, unsigned short actual,
                           const char *file , int lineNo)
{       
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %u, got: %u", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the unsigned shorts are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *     
 */      
void assertNotEqUnsignedShort (unsigned short expected, unsigned short actual,
                              const char *file , int lineNo)
{      
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %u, got: %u", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}


/**
 * 
 *  Asserts if the integers are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqInt (int expected, int actual,
                 const char *file , int lineNo)
{       
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the integers are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqInt (int expected, int actual,
                    const char *file , int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %d, got: %d", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 * 
 *  Asserts if the unsigned integers are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqUnsignedInt (unsigned int expected, unsigned int actual,
                         const char *file , int lineNo)
{       
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %u, got: %u", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the unsigned integers are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqUnsignedInt (unsigned int expected, unsigned int actual,
                            const char *file , int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %u, got: %u", 
                    expected, actual);
   
    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the longs are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqLong (long expected, long actual,
                  const char *file , int lineNo)
{        
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %d, got: %d", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the longs are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqLong (long expected, long actual,
                     const char *file, int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %d, got: %d", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}


/**
 *  Asserts if the unsigned longs are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqUnsignedLong (unsigned long expected, unsigned long actual,
                          const char *file , int lineNo)
{        
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected != actual)
  {
    // create error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %u, got: %u", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the unsigned longs are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqUnsignedLong (unsigned long expected, unsigned long actual,
                             const char *file, int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];
 
  if (expected == actual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %u, got: %u", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}

#ifdef FLOATING_POINT_NEEDED

/**
 *  Asserts if the floats are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqFloat (float expected, float actual,
                   const char *file, int lineNo)
{
  assertEqDouble((double) expected, (double) actual,
    file, lineNo);
}

/**
 *  Asserts if the floats are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqFloat (float expected, float actual,
                      const char *file, int lineNo)
{
  assertNotEqDouble((double) expected, (double) actual,
    file, lineNo);
}

/**
 *  Asserts if the floats are not close enough
 *  in absolute terms.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param absDiff  the absolute difference allowed
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertFloatClose (float expected, float actual, float absDiff,
                      const char *file, int lineNo)
{
  assertLongDoubleClose((double) expected, (double) actual, (double) absDiff,
    file, lineNo);
}

/**
 *  Asserts if the doubles are not equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertEqDouble (double expected, double actual,
                    const char *file, int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];
  bool isEqual         = false;
  double relativeError = (double) 0.0;

  // do explicit checks for zero to avoid any divide-by-zero issues
  if((double) 0.0 == expected)
  {
    isEqual = (fabs(actual) < MAX_FLOAT_ABSOLUTE_ERROR)
      ? true : false;
  }
  else if((double) 0.0 == actual)
  {
    isEqual = (fabs(expected) < MAX_FLOAT_ABSOLUTE_ERROR)
      ? true : false;
  }
  else if(fabs(expected - actual) < MAX_FLOAT_ABSOLUTE_ERROR)
  {
    // see if they are close enough in absolute terms
    isEqual = true;
  }
  else
  {
    // see if they are close enough in relative terms,
    if(fabs(expected) > fabs(actual))
    {
      relativeError = fabs((expected - actual) / expected);
    }
    else
    {
      relativeError = fabs((actual - expected) / actual);
    }

    if(relativeError < MAX_FLOAT_RELATIVE_ERROR)
    {
      isEqual = true;
    }
  }

  if(!isEqual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %e, got: %e", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the doubles are equal.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertNotEqDouble (double expected, double actual,
                       const char *file, int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];
  bool isEqual         = false;
  double relativeError = 0.0;

  // do explicit checks for zero to avoid any divide-by-zero issues
  if((double) 0.0 == expected)
  {
    isEqual = (fabs(actual) < MAX_FLOAT_ABSOLUTE_ERROR)
      ? true : false;
  }
  else if((double) 0.0 == actual)
  {
    isEqual = (fabs(expected) < MAX_FLOAT_ABSOLUTE_ERROR)
      ? true : false;
  }
  else if(fabs(expected - actual) < MAX_FLOAT_ABSOLUTE_ERROR)
  {
    // see if they are close enough in absolute terms
    isEqual = true;
  }
  else
  {
    // see if they are close enough in relative terms
    if(fabs(expected) > fabs(actual))
    {
      relativeError = fabs((expected - actual) / expected);
    }
    else
    {
      relativeError = fabs((actual - expected) / actual);
    }

    if(relativeError < MAX_FLOAT_RELATIVE_ERROR)
    {
      isEqual = true;
    }
  }

  if(isEqual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should not be equal to: %e, got: %e", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the doubles are not close enough in
 *  absolute terms.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param absDiff  the absolute difference allowed
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertDoubleClose (double expected, double actual, double absDiff,
                       const char *file, int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];

  bool isEqual = (fabs(expected - actual) < absDiff)
    ? true : false;

  if(!isEqual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %e, got: %e", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}

/**
 *  Asserts if the long doubles are not close enough in
 *  absolute terms.
 *     
 *  @param expected the expected value
 *  @param actual   the value to compare to
 *  @param absDiff  the absolute difference allowed
 *  @param file     the source file name
 *  @param lineNo   the source code line number
 *
 */      
void assertLongDoubleClose (long double expected, long double actual,
                       long double absDiff, const char *file, int lineNo)
{     
  char errorMsg[MAX_MSG_LEN + 1];

  bool isEqual = (fabs(expected - actual) < absDiff)
    ? true : false;

  if(!isEqual)
  {
    // create an error message with details
    (void) snprintf(errorMsg, MAX_MSG_LEN,
                    " should be equal to: %.15Le, got: %.15Le", 
                    expected, actual);

    assertFailed(file, lineNo, errorMsg);
  }
}

#endif // FLOATING_POINT_NEEDED
